/*
 * Copyright 2002-2018 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.springframework.ejb.access;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import javax.naming.Context;
import javax.naming.NamingException;

import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;

import org.springframework.jndi.JndiObjectLocator;
import org.springframework.lang.Nullable;

/**
 * Base class for AOP interceptors invoking local or remote Stateless Session Beans.
 * Designed for EJB 2.x, but works for EJB 3 Session Beans as well.
 *
 * <p>Such an interceptor must be the last interceptor in the advice chain.
 * In this case, there is no direct target object: The call is handled in a
 * special way, getting executed on an EJB instance retrieved via an EJB home.
 *
 * @author Rod Johnson
 * @author Juergen Hoeller
 */
public abstract class AbstractSlsbInvokerInterceptor extends JndiObjectLocator
        implements MethodInterceptor {

    private boolean lookupHomeOnStartup = true;

    private boolean cacheHome = true;

    private boolean exposeAccessContext = false;

    /**
     * The EJB's home object, potentially cached.
     * The type must be Object as it could be either EJBHome or EJBLocalHome.
     */
    @Nullable
    private Object cachedHome;

    /**
     * The no-arg create() method required on EJB homes, potentially cached.
     */
    @Nullable
    private Method createMethod;

    private final Object homeMonitor = new Object();


    /**
     * Set whether to look up the EJB home object on startup.
     * Default is "true".
     * <p>Can be turned off to allow for late start of the EJB server.
     * In this case, the EJB home object will be fetched on first access.
     *
     * @see #setCacheHome
     */
    public void setLookupHomeOnStartup(boolean lookupHomeOnStartup) {
        this.lookupHomeOnStartup = lookupHomeOnStartup;
    }

    /**
     * Set whether to cache the EJB home object once it has been located.
     * Default is "true".
     * <p>Can be turned off to allow for hot restart of the EJB server.
     * In this case, the EJB home object will be fetched for each invocation.
     *
     * @see #setLookupHomeOnStartup
     */
    public void setCacheHome(boolean cacheHome) {
        this.cacheHome = cacheHome;
    }

    /**
     * Set whether to expose the JNDI environment context for all access to the target
     * EJB, i.e. for all method invocations on the exposed object reference.
     * <p>Default is "false", i.e. to only expose the JNDI context for object lookup.
     * Switch this flag to "true" in order to expose the JNDI environment (including
     * the authorization context) for each EJB invocation, as needed by WebLogic
     * for EJBs with authorization requirements.
     */
    public void setExposeAccessContext(boolean exposeAccessContext) {
        this.exposeAccessContext = exposeAccessContext;
    }


    /**
     * Fetches EJB home on startup, if necessary.
     *
     * @see #setLookupHomeOnStartup
     * @see #refreshHome
     */
    @Override
    public void afterPropertiesSet() throws NamingException {
        super.afterPropertiesSet();
        if (this.lookupHomeOnStartup) {
            // look up EJB home and create method
            refreshHome();
        }
    }

    /**
     * Refresh the cached home object, if applicable.
     * Also caches the create method on the home object.
     *
     * @throws NamingException if thrown by the JNDI lookup
     * @see #lookup
     * @see #getCreateMethod
     */
    protected void refreshHome() throws NamingException {
        synchronized (this.homeMonitor) {
            Object home = lookup();
            if (this.cacheHome) {
                this.cachedHome = home;
                this.createMethod = getCreateMethod(home);
            }
        }
    }

    /**
     * Determine the create method of the given EJB home object.
     *
     * @param home the EJB home object
     * @return the create method
     * @throws EjbAccessException if the method couldn't be retrieved
     */
    @Nullable
    protected Method getCreateMethod(Object home) throws EjbAccessException {
        try {
            // Cache the EJB create() method that must be declared on the home interface.
            return home.getClass().getMethod("create");
        } catch (NoSuchMethodException ex) {
            throw new EjbAccessException("EJB home [" + home + "] has no no-arg create() method");
        }
    }

    /**
     * Return the EJB home object to use. Called for each invocation.
     * <p>Default implementation returns the home created on initialization,
     * if any; else, it invokes lookup to get a new proxy for each invocation.
     * <p>Can be overridden in subclasses, for example to cache a home object
     * for a given amount of time before recreating it, or to test the home
     * object whether it is still alive.
     *
     * @return the EJB home object to use for an invocation
     * @throws NamingException if proxy creation failed
     * @see #lookup
     * @see #getCreateMethod
     */
    protected Object getHome() throws NamingException {
        if (!this.cacheHome || (this.lookupHomeOnStartup && !isHomeRefreshable())) {
            return (this.cachedHome != null ? this.cachedHome : lookup());
        } else {
            synchronized (this.homeMonitor) {
                if (this.cachedHome == null) {
                    this.cachedHome = lookup();
                    this.createMethod = getCreateMethod(this.cachedHome);
                }
                return this.cachedHome;
            }
        }
    }

    /**
     * Return whether the cached EJB home object is potentially
     * subject to on-demand refreshing. Default is "false".
     */
    protected boolean isHomeRefreshable() {
        return false;
    }


    /**
     * Prepares the thread context if necessar, and delegates to
     * {@link #invokeInContext}.
     */
    @Override
    @Nullable
    public Object invoke(MethodInvocation invocation) throws Throwable {
        Context ctx = (this.exposeAccessContext ? getJndiTemplate().getContext() : null);
        try {
            return invokeInContext(invocation);
        } finally {
            getJndiTemplate().releaseContext(ctx);
        }
    }

    /**
     * Perform the given invocation on the current EJB home,
     * within the thread context being prepared accordingly.
     * Template method to be implemented by subclasses.
     *
     * @param invocation the AOP method invocation
     * @return the invocation result, if any
     * @throws Throwable in case of invocation failure
     */
    @Nullable
    protected abstract Object invokeInContext(MethodInvocation invocation) throws Throwable;


    /**
     * Invokes the {@code create()} method on the cached EJB home object.
     *
     * @return a new EJBObject or EJBLocalObject
     * @throws NamingException           if thrown by JNDI
     * @throws InvocationTargetException if thrown by the create method
     */
    protected Object create() throws NamingException, InvocationTargetException {
        try {
            Object home = getHome();
            Method createMethodToUse = this.createMethod;
            if (createMethodToUse == null) {
                createMethodToUse = getCreateMethod(home);
            }
            if (createMethodToUse == null) {
                return home;
            }
            // Invoke create() method on EJB home object.
            return createMethodToUse.invoke(home, (Object[]) null);
        } catch (IllegalAccessException ex) {
            throw new EjbAccessException("Could not access EJB home create() method", ex);
        }
    }

}
